﻿namespace Legend
{
    using System;
    using System.Collections.Generic;
    using System.Globalization;

    /// <summary>
    /// Provides static methods and extensions for the IMaybe-type.
    /// </summary>
    public static class Maybe
    {
        /// <summary>
        /// Gets an IMaybe&lt;T&gt; representing the specified value.
        /// </summary>
        /// <typeparam name="T">The type of value.</typeparam>
        /// <param name="value">The value to wrap in a maybe.</param>
        /// <returns>A maybe representing the specified value.</returns>
        public static Maybe<T> Return<T>(T value)
        {
            return Maybe<T>.Just(value);
        }

        /// <summary>
        /// Provides a select many function for the maybe monad so that it can be
        /// used in LINQ expressions. Has the same behavior as Bind with the added
        /// selector lambda that projects the result of the bound monads to a new type.
        /// </summary>
        /// <typeparam name="T1">The type of the first monad.</typeparam>
        /// <typeparam name="T2">The type of the second monad.</typeparam>
        /// <typeparam name="TResult">The result of the selector.</typeparam>
        /// <param name="maybe">The first maybe.</param>
        /// <param name="binder">The binder for the maybe.</param>
        /// <param name="selector">A projection.</param>
        /// <returns>A maybe with the selected result.</returns>
        public static Maybe<TResult> SelectMany<T1, T2, TResult>(this Maybe<T1> maybe, Func<T1, Maybe<T2>> binder, Func<T1, T2, TResult> selector)
        {
            Require.ThatArgument(maybe).Named("maybe").IsNotNull();
            Require.ThatArgument(binder).Named("binder").IsNotNull();
            Require.ThatArgument(selector).Named("selector").IsNotNull();

            return maybe.Bind(x => binder(x).Bind(y => Return(selector(x, y))));
        }

        /// <summary>
        /// Provides the where operator for LINQ-expressions.
        /// </summary>
        /// <typeparam name="T">The type of the maybe.</typeparam>
        /// <param name="maybe">The maybe to filter.</param>
        /// <param name="predicate">A predicate over the value of the maybe.</param>
        /// <returns>If the predicate returns true it returns Just the value of the monad, otherwise Nothing.</returns>
        public static Maybe<T> Where<T>(this Maybe<T> maybe, Func<T, bool> predicate)
        {
            return maybe.Bind(x => predicate(x) ? Return(x) : Nothing<T>());
        }

        /// <summary>
        /// Maybe gets the value if it's not null.
        /// </summary>
        /// <typeparam name="T">The type of the value.</typeparam>
        /// <param name="value">The value.</param>
        /// <returns>A maybe representing the non null value.</returns>
        public static Maybe<T> NotNull<T>(T value) where T : class
        {
            return value != null ? Return(value) : Nothing<T>();
        }

        /// <summary>
        /// Maybe gets the value if it's not null.
        /// </summary>
        /// <typeparam name="T">The type of the value.</typeparam>
        /// <param name="value">The value.</param>
        /// <returns>A maybe representing the non null value.</returns>
        public static Maybe<T?> NotNull<T>(T? value) where T : struct
        {
            return value != null ? Return(value) : Nothing<T?>();
        }

        /// <summary>
        /// Maybe performs the specified action.
        /// </summary>
        /// <typeparam name="T">The type of maybe.</typeparam>
        /// <param name="maybe">The maybe to bind to.</param>
        /// <param name="action">The action to perform.</param>
        public static void Do<T>(this Maybe<T> maybe, Action<T> action)
        {
            Require.ThatArgument(maybe, "maybe").IsNotNull();
            Require.ThatArgument(action, "action").IsNotNull();

            maybe.Bind(x =>
            {
                action(x);
                return maybe;
            });
        }

        /// <summary>
        /// Projects the value in the monad through the selector.
        /// </summary>
        /// <typeparam name="T">The type of value in the maybe.</typeparam>
        /// <typeparam name="TReturn">The type of the projection.</typeparam>
        /// <param name="maybe">The maybe to project over.</param>
        /// <param name="selector">A projection.</param>
        /// <returns>A maybe with result of the selector.</returns>
        public static Maybe<TReturn> Select<T, TReturn>(this Maybe<T> maybe, Func<T, TReturn> selector)
        {
            Require.ThatArgument(maybe, "maybe").IsNotNull();
            Require.ThatArgument(selector, "selector").IsNotNull();

            return maybe.Bind(x => Return(selector(x)));
        }

        /// <summary>
        /// Maybe parses the specified string to an integer.
        /// </summary>
        /// <param name="value">The value to parse.</param>
        /// <returns>A maybe containing the result of the parsing.</returns>
        public static Maybe<int> ParseInt(this string value)
        {
            Require.ThatArgument(value, "value").IsNotNull();

            return value.ParseInt(NumberStyles.Any, CultureInfo.CurrentCulture);
        }

        /// <summary>
        /// Maybe parses the specified string to an integer.
        /// </summary>
        /// <param name="value">The value to parse.</param>
        /// <param name="style">Number styles to use.</param>
        /// <param name="provider">The format provider to use.</param>
        /// <returns>A maybe containing the result of the parsing.</returns>
        public static Maybe<int> ParseInt(this string value, NumberStyles style, IFormatProvider provider)
        {
            Require.ThatArgument(value).Named("value").IsNotNull();
            Require.ThatArgument(provider).Named("provider").IsNotNull();

            int result;

            if (!int.TryParse(value, style, provider, out result))
            {
                return Nothing<int>();
            }

            return Return(result);
        }

        /// <summary>
        /// Maybe parses the specified value to a bool.
        /// </summary>
        /// <param name="value">The value to parse.</param>
        /// <returns>A maybe containing the result from the parsing.</returns>
        public static Maybe<bool> ParseBool(this string value)
        {
            Require.ThatArgument(value).Named("value").IsNotNull();

            bool result;
            return bool.TryParse(value, out result) ? Return(result) : Nothing<bool>();
        }

        /// <summary>
        /// Maybe converts the string value to a boolean. If the isTrueValue predicate returns true
        /// the conversion will return true, if not so the isFalseValue predicate is evaluated, if that is true the
        /// conversion will return false, if both predicates are false the conversion will not succeed.
        /// </summary>
        /// <param name="value">The value to convert.</param>
        /// <param name="isTrueValue">A predicate for true values.</param>
        /// <param name="isFalseValue">A predicate for false values.</param>
        /// <returns>A maybe monad with the result of the conversion.</returns>
        public static Maybe<bool> ToBoolean(this string value, Func<string, bool> isTrueValue, Func<string, bool> isFalseValue)
        {
            Require.ThatArgument(value).Named("value").IsNotNull();
            Require.ThatArgument(isTrueValue).Named("isTrueValue").IsNotNull();
            Require.ThatArgument(isFalseValue).Named("isFalseValue").IsNotNull();

            var isTrue = isTrueValue(value);
            return isTrue || isFalseValue(value) ? Return(isTrue) : Nothing<bool>();
        }

        /// <summary>
        /// Maybe gets the value of the specified key in the dictionary.
        /// </summary>
        /// <typeparam name="TKey">The type of keys in the dictionary.</typeparam>
        /// <typeparam name="TValue">The type of values in the dictionary.</typeparam>
        /// <param name="dictionary">The dictionary to get the value from.</param>
        /// <param name="key">The key to get the value for.</param>
        /// <returns>A maybe with the value if it exists, otherwise a nothing maybe.</returns>
        public static Maybe<TValue> GetValue<TKey, TValue>(this IDictionary<TKey, TValue> dictionary, TKey key)
        {
            Require.ThatArgument(dictionary).Named("dictionary").IsNotNull();

            TValue result;

            if (!dictionary.TryGetValue(key, out result))
            {
                return Maybe<TValue>.Nothing;
            }

            return Return(result);
        }

        /// <summary>
        /// Maybe invokes the function represented by the maybe with the specified value.
        /// </summary>
        /// <typeparam name="T">The type of value.</typeparam>
        /// <typeparam name="TReturn">The return type of the function.</typeparam>
        /// <param name="maybe">The maybe.</param>
        /// <param name="value">The vale to invoke with.</param>
        /// <returns>A maybe with the result of the invokation.</returns>
        public static Maybe<TReturn> Invoke<T, TReturn>(this Maybe<Func<T, Maybe<TReturn>>> maybe, T value)
        {
            Require.ThatArgument(maybe).Named("maybe").IsNotNull();

            return maybe.Bind(x => x.Invoke(value));
        }

        /// <summary>
        /// Gets the value for the specified monad or the default value for the type if the monad
        /// is Nothing.
        /// </summary>
        /// <typeparam name="T">The type of value.</typeparam>
        /// <param name="maybe">The maybe monad to get the value from.</param>
        /// <returns>The value or the default value of the type T.</returns>
        public static T GetValueOrDefault<T>(this Maybe<T> maybe)
        {
            Require.ThatArgument(maybe).Named("maybe").IsNotNull();

            var result = default(T);
            maybe.Bind(x =>
            {
                result = x;
                return maybe;
            });
            return result;
        }

        private static Maybe<T> Nothing<T>()
        {
            return Maybe<T>.Nothing;
        }
    }

    /// <summary>
    /// Provides a way to "get into" the maybe monad.
    /// </summary>
    /// <typeparam name="T">The type of value for the monad.</typeparam>
    public abstract class Maybe<T>
    {
        private Maybe()
        {
        }

        /// <summary>
        /// Represents a nothing value for the specified type.
        /// </summary>
        public static readonly Maybe<T> Nothing = new NothingMaybe();

        /// <summary>
        /// Creates a maybe that represents the specified value.
        /// </summary>
        /// <param name="value">The value to return.</param>
        /// <returns>A maybe monad with the specified value.</returns>
        public static Maybe<T> Just(T value)
        {
            return new JustMaybe(value);
        }

        public abstract Maybe<TReturn> Bind<TReturn>(Func<T, Maybe<TReturn>> binder);

        public abstract Maybe<TReturn> TryCast<TReturn>();

        public static implicit operator Maybe<object>(Maybe<T> maybe)
        {
            return maybe.TryCast<object>();
        }

        private class NothingMaybe
            : Maybe<T>
        {
            public override Maybe<TReturn> Bind<TReturn>(Func<T, Maybe<TReturn>> binder)
            {
                return Maybe<TReturn>.Nothing;
            }

            public override Maybe<TReturn> TryCast<TReturn>()
            {
                return Maybe<TReturn>.Nothing;
            }
        }

        private class JustMaybe
            : Maybe<T>
        {
            private readonly T value;

            public JustMaybe(T value)
            {
                this.value = value;
            }

            public override Maybe<TReturn> Bind<TReturn>(Func<T, Maybe<TReturn>> binder)
            {
                return binder(this.value);
            }

            public override Maybe<TReturn> TryCast<TReturn>()
            {
                if (!this.CanCastTo<TReturn>())
                {
                    return Maybe<TReturn>.Nothing;
                }

                return Maybe.Return((TReturn)(object)this.value);
            }

            private bool CanCastTo<TReturn>()
            {
                var returnType = typeof (TReturn);

                return (this.value == null && !returnType.IsValueType) || returnType.IsAssignableFrom(this.value.GetType());
            }

            public override string ToString()
            {
                return this.value != null ? this.value.ToString() : "NULL";
            }
        }

    }
}